import { Bot, Resource, DiscordApplicationDataResponse } from '@boticord/resources-client'
import { BadRequestException, CACHE_MANAGER, forwardRef, Inject, Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Cache } from 'cache-manager'
import { Repository } from 'typeorm'
import { BotsService } from './bots.service'
import { ServersService } from './servers.service'
import { ErrorCodes, Limits } from '@boticord/consts'
import { wait } from 'discordoo'
import { ConfigService } from '@nestjs/config'
import { CommentsService } from './comments.service'
import { QualityCheckMethods } from '@boticord/consts/quality-check-methods'

export type MeiliIndexedResource = 'bot' | 'server' | 'comment'

@Injectable()
export class ResourcesService {
  constructor(
    @InjectRepository(Resource) private readonly resources: Repository<Resource>,
    @Inject(CACHE_MANAGER) private readonly cache: Cache,   
    @Inject(forwardRef(() => BotsService)) private readonly botsService: BotsService,
    @Inject(forwardRef(() => ServersService)) private readonly serversService: ServersService,
    @Inject(forwardRef(() => CommentsService)) private readonly commentsService: CommentsService,
    private readonly config: ConfigService,
  ) {}

  private resourceTypeKey = (id: string) => `resource-type:${id}` 

  async indexate(id: string) {
    const type = await this.getType(id)
    switch (type) {
      case 'bot':
        await this.botsService.indexate(id)
        break
      case 'server':
        await this.serversService.indexate(id)
        break
      case 'comment':
        await this.commentsService.indexate(id)
        break
    }
  }

  async saveTypeCache(id: string, type: MeiliIndexedResource) {
    await this.cache.set(this.resourceTypeKey(id), type, { ttl: 0 }) // ttl = 0 means infinity
  }

  async getType(id: string): Promise<MeiliIndexedResource> {
    const cached = await this.cache.get<MeiliIndexedResource>(this.resourceTypeKey(id))

    if (cached) return cached

    const serverOrBot = await this.get(id)
    if (serverOrBot) {
      const type = serverOrBot instanceof Bot ? 'bot' : 'server'
      await this.saveTypeCache(id, type)
      return type
    } else {
      await this.saveTypeCache(id, 'comment')
      return 'comment'
    }
  }

  async get(id: string) {
    return this.resources.findOne({ where: { id } })
  }

  async checkMonitoringBot(id: string, method: QualityCheckMethods) {
    const worker: string = this.config.getOrThrow('WORKER_MON_CHECK_ENDPOINT')

    const init: RequestInit = {
      method: 'GET',
      headers: {
        'CF-Access-Client-Id': this.config.get('CF_ACCESS_MON_CHECK_CLIENT_ID') ?? '',
        'CF-Access-Client-Secret': this.config.get('CF_ACCESS_MON_CHECK_CLIENT_SECRET') ?? '',
      },
    }

    const err = { ok: false, result: { approved: false }, errors: [ { code: ErrorCodes.UnknownError } ] }
    let monitoring = ''

    switch (method) {
      case QualityCheckMethods.Topgg:
        monitoring = 'topgg'
        break
      case QualityCheckMethods.DiscordBotList:
        monitoring = 'dbl'
        break
      case QualityCheckMethods.BotsOnDiscord:
        monitoring = 'botsondiscord'
        break
      case QualityCheckMethods.DiscordBots:
        monitoring = 'discordbots'
        break
      default:
        return err
    }

    const response = await fetch(`${worker.endsWith('/') ? worker.slice(0, -1) : worker}/${monitoring}/${id}`, init)
      .catch(() => {
        return { json: async () => err }
      })

    return await response.json()
      .catch(() => err) as { ok: boolean; result: { approved: boolean }; errors?: Array<{ code: ErrorCodes }> }
  }

  async fetchApplicationData(id: string): Promise<DiscordApplicationDataResponse | null> {
    if (id.length < Limits.MinDiscordIdLength || id.length > Limits.MaxDiscordIdLength) {
      throw new BadRequestException()
    }

    let data = await this.makeRequest(id)
    if (!data.application) {
      await wait(700) // if application fetched first time, it can appear in api with delay
      data = await this.makeRequest(id)
      if (!data.application) return null
    }

    return data
  }

  private async makeRequest(id: string): Promise<DiscordApplicationDataResponse> {
    const worker = this.config.get('WORKER_BOT_FETCH_ENDPOINT')
    if (!worker) return { application: undefined }

    const request: RequestInit = {
      method: 'GET',
      headers: {
        'CF-Access-Client-Id': this.config.get('CF_ACCESS_BOT_FETCH_CLIENT_ID') ?? '',
        'CF-Access-Client-Secret': this.config.get('CF_ACCESS_BOT_FETCH_CLIENT_SECRET') ?? '',
      },
      redirect: 'follow',
    }

    const response = await fetch(`${worker.endsWith('/') ? worker.slice(0, -1) : worker}/${id}`, request)
      .catch(() => {
        return { json: async () => { return { application: undefined } } }
      })

    return await response.json().catch(() => {
      return { application: undefined }
    })
  }
}